﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Google.GData.Calendar;
using Google.GData.Client;
using System.Net;
using Google.GData.Extensions;

namespace TasksUI.Helpers
{
    /// <summary>
    /// http://code.google.com/apis/calendar/data/2.0/reference.html
    /// not ready yet, all properties of task have to be mapped to some xml properties of googles task
    /// TODO validate operation order
    /// TODO finish implementation
    /// </summary>
    public class GoogleHelper : ISynchronizer
    {
        public bool UseSSL { get; set; }

        public GoogleHelper()
            : this(WebRequest.GetSystemWebProxy())
        {
        }

        public GoogleHelper(IWebProxy proxy)
        {
            WebProxy myProxy = new WebProxy(proxy.GetProxy(new Uri(mainUrl)));
            myProxy.Credentials = CredentialCache.DefaultCredentials;
            myProxy.UseDefaultCredentials = true;
            myProxy.BypassProxyOnLocal = true;

            GDataGAuthRequestFactory requestFactory = (GDataGAuthRequestFactory)_svc.RequestFactory;
            requestFactory.Proxy = myProxy;
            requestFactory.UseGZip = true;
            requestFactory.KeepAlive = true;

            WebRequest.DefaultWebProxy.Credentials = CredentialCache.DefaultNetworkCredentials; 
        }

        private static readonly string mainUrl = "http://www.google.com/calendar/feeds/default/owncalendars/full";

        private readonly CalendarService _svc = new CalendarService("mp-Tasks-1");

        public bool Connect(string login, string password)
        {
            // Create a CalenderService and authenticate
            _svc.setUserCredentials(login, password);
            return true;
        }

        public List<CalendarWrapper> GetCalendars()
        {
            CalendarQuery query = new CalendarQuery();
            if (UseSSL)
                query.Uri = new Uri(mainUrl.Replace("http:", "https:"));
            else
                query.Uri = new Uri(mainUrl);
            query.FeedFormat = AlternativeFormat.Atom;
            query.UseSSL = UseSSL;

            CalendarFeed resultFeed = (CalendarFeed)_svc.Query(query);

            List<CalendarWrapper> res = new List<CalendarWrapper>();
            foreach (CalendarEntry entry in resultFeed.Entries)
            {
                res.Add(new CalendarWrapper(entry));
            }
            return res;
        }

        public sealed class CalendarWrapper
        {
            private readonly string url;

            private readonly CalendarEntry entry;

            public CalendarWrapper(string calendar)
            {
                this.entry = new CalendarEntry();
                url = calendar;
                System.Text.RegularExpressions.Match m = System.Text.RegularExpressions.Regex.Match(
                    url, "/([^%/]+%40[^/]+)/");
                if (m != null && m.Success)
                    entry.Title.Text = m.Groups[1].Value.Replace("%40", "@");
                else
                    entry.Title.Text = calendar;
            }

            public CalendarWrapper(CalendarEntry entry)
            {
                // find proper link
                AtomLink al = entry.Links.FirstOrDefault(delegate(AtomLink l)
                {
                    // http://www.google.com/calendar/feeds/login%40gmail.com/private/full
                    return l.Rel == "http://schemas.google.com/gCal/2005#eventFeed";
                });
                url = al.AbsoluteUri;
                this.entry = entry;
            }

            public string Title { get { return entry.Title.Text; } }

            public string Url { get { return url; } }
        }

        public void Synchronize(TasksModel.TaskLibrary tl, bool testOnly, EventHandler<SynchProgress> progress)
        {
            // due date or finished date is required
            //t.NoSynch && !t.DueDate.HasValue && !t.FinishedAt.HasValue
            System.Diagnostics.Debug.Assert(tl.SynchSettings is TasksModel.GoogleCalendarSynchSettings);
            SynchProgress sp = new SynchProgress();
            TasksModel.SynchSettings.SynchDirection mode = tl.SynchSettings.Mode;

            // try to support real cancellation
            List<TasksModel.Task> toAdd = new List<TasksModel.Task>(1);
            List<TasksModel.Task> toRemove = new List<TasksModel.Task>(1);
            Dictionary<TasksModel.Task, EventEntry> toUpdate = new Dictionary<TasksModel.Task, EventEntry>(1);
            
            //TODO filter by last update time > lowest synch time
            List<EventEntry> events = GetEvents(
                ((TasksModel.GoogleCalendarSynchSettings)tl.SynchSettings).Calendar,
                DateTime.Now.AddMonths(-2), DateTime.Now.AddDays(1));
            Save(events);
            //return;
            sp.TotalItems = events.Count + 1;
            switch (mode)
            {
                case TasksModel.SynchSettings.SynchDirection.FromExternalOnly:
                    // match local with external
                    Import(tl, progress, sp, events, toAdd, toUpdate);
                    break;
                case TasksModel.SynchSettings.SynchDirection.ToExternalOnly:
                    // match external with local except archived
                    Export(tl, progress, sp, events);
                    break;
                case TasksModel.SynchSettings.SynchDirection.TwoWay:
                    break;
            }
            /*toUpdate.BatchData = new GDataBatchEntryData("A", GDataBatchOperationType.update);
                toDelete.BatchData = new GDataBatchEntryData("C", GDataBatchOperationType.delete);
                toCreate.BatchData = new GDataBatchEntryData("D", GDataBatchOperationType.insert);
                AtomFeed batchFeed = new AtomFeed(feed);
                batchFeed.Entries.Add(toUpdate);
                EventFeed batchResultFeed = (EventFeed)service.Batch(batchFeed, new Uri(feed.Batch));
                //check the return values of the batch operations to make sure they all worked.
                //the insert operation should return a 201 and the rest should return 200
                bool success = true;
                foreach (EventEntry entry in batchResultFeed.Entries)
                {
                    if (entry.BatchData.Status.Code != 200 && entry.BatchData.Status.Code != 201)
                    {
                        success = false;
                        Console.WriteLine("The batch operation with ID " +
                            entry.BatchData.Id + " failed.");
                    }
                }

*/
            // no cancellation beyond this point
            foreach(TasksModel.Task t in toAdd)
                tl.Tasks.AddTask(t.Class, t);
            foreach (KeyValuePair<TasksModel.Task, EventEntry> kv in toUpdate)
                UpdateTaskFromEvent(kv.Key, kv.Value);
            foreach (TasksModel.Task t in toRemove)
                tl.Tasks.Remove(t);
            if (progress != null)
            {
                sp.CompletedItems = sp.TotalItems;
                progress(this, sp);
            }
        }

        private void Save(List<EventEntry> events)
        {
            foreach (EventEntry e in events)
            {
                using (var sw = new System.Xml.XmlTextWriter(e.Uid.Value + ".xml", System.Text.Encoding.UTF8))
                {
                    sw.Formatting = System.Xml.Formatting.Indented;
                    ObjectDumper.Write(e, sw);
                }
            }
        }

        private void Export(TasksModel.TaskLibrary tl, EventHandler<SynchProgress> progress, 
            SynchProgress sp, List<EventEntry> events)
        {
            foreach (TasksModel.Task task in tl.Tasks.AllTasks)
            {
                if (task.NoSynch)
                    continue;
                // try to find existing task
                string tid = task.ID.ToString("D");
                EventEntry entry = task.ExternalReference == null ?
                    null : events.Find(delegate(EventEntry ee)
                {
                    string id = GetExtProperty(ee, TasksModel.Task.BaseSchema + "task/id");
                    return id == tid;
                });
                if (entry != null)
                {
                    // update
                    entry = UpdateTask(entry, task);
                }
                else
                    // add new
                    entry = AddTask(task, ((TasksModel.GoogleCalendarSynchSettings)tl.SynchSettings).Calendar);
                System.Diagnostics.Debug.Assert(task.ExternalReference != null);
                if (progress != null)
                {
                    sp.CompletedItems++;
                    progress(this, sp);
                    if (sp.Cancel)
                        break;
                }
            }
        }

        private void Import(TasksModel.TaskLibrary tl, EventHandler<SynchProgress> progress,
            SynchProgress sp, List<EventEntry> events,
            List<TasksModel.Task> toAdd, Dictionary<TasksModel.Task, EventEntry> toUpdate)
        {
            Guid tid;
            foreach (EventEntry ee in events)
            {
                TasksModel.Task task = null;
                // for each event, try to find existing task
                string id = GetExtProperty(ee, TasksModel.Task.BaseSchema + "task/id");
                if (!string.IsNullOrEmpty(id))
                {
                    tid = new Guid(id);
                    task = tl.Tasks.Find(tid, true);
                }
                if (task == null)
                {
                    // try to find existing by external reference
                    List<TasksModel.Task> t =
                        tl.Tasks.SearchByExtReference(ee.SelfUri.Content, true);
                    if (t.Count > 0)
                        task = t[0];
                }
                // found, update local task data
                if (task != null)
                    toUpdate.Add(task, ee);
                else
                {
                    // create a new one
                    task = new TasksModel.Task();
                    task.Class = TasksModel.TaskClass.ImportantNotUrgent;
                    task.Priority = TasksModel.PriorityClass.Medium;
                    UpdateTaskFromEvent(task, ee);
                    toAdd.Add(task);
                }
                if (progress != null)
                {
                    sp.CompletedItems++;
                    progress(this, sp);
                    if (sp.Cancel)
                        break;
                }
            }
        }

        public List<EventEntry> GetEvents(string calendar, DateTime start, DateTime end)
        {
            EventQuery query = new EventQuery();
            if (UseSSL)
                query.Uri = new Uri(calendar.Replace("http:", "https:"));
            else
                query.Uri = new Uri(calendar);
            query.FeedFormat = AlternativeFormat.Atom;
            query.UseSSL = UseSSL;
            //TODO query.TimeZone
            //query.ExtraParameters = "showdeleted=true";
            query.NumberToRetrieve = 100;
            query.FutureEvents = false;
            query.SingleEvents = false;
            query.SortOrder = CalendarSortOrder.ascending;
            query.StartTime = start;
            query.EndTime = end;
            List<EventEntry> res = new List<EventEntry>();
            do
            {
                EventFeed ef = _svc.Query(query);
                foreach (EventEntry e in ef.Entries)
                    res.Add(e);
                query.StartIndex = ef.Entries.Count;
            } while (query.StartIndex == query.NumberToRetrieve);
            return res;
        }

        private EventEntry GetEvent(string id)
        {
            //http://www.google.com/calendar/feeds/userID/visibility/projection/eventID
            EventQuery query = new EventQuery();
            if (UseSSL)
                query.Uri = new Uri(id.Replace("http:", "https:"));
            else
                query.Uri = new Uri(id);
            EventFeed feed = _svc.Query(query);
            if (feed.Entries.Count > 0)
                return feed.Entries[0] as EventEntry;
            return null;
        }

        public EventEntry AddTask(TasksModel.Task t, string calendar)
        {
            EventEntry entry = new EventEntry();
            FillEntry(entry, t);
            // https://www.google.com/calendar/feeds/login%40gmail.com/private/full";
            Uri postUri = new Uri(UseSSL ? calendar.Replace("http:", "https:") : calendar);
            EventEntry insertedEntry = _svc.Insert(postUri, entry);
            t.ExternalReference = insertedEntry.SelfUri.Content;// uri of this task
            return insertedEntry;
        }

        public EventEntry UpdateTask(EventEntry entry, TasksModel.Task t)
        {
            FillEntry(entry, t);
            EventEntry updatedEntry = _svc.Update(entry);
            return updatedEntry;
        }

        #region Private

        private static void FillEntry(EventEntry entry, TasksModel.Task t)
        {
            entry.Uid = new GCalUid(t.ID.ToString("D"));
            entry.Title.Text = t.Name;
            entry.Content.Type = "rtf";
            entry.Content.Content = t.RichContent;
            entry.Summary.Type = AtomTextConstructType.text;
            entry.Summary.Text = t.RichContent;

            AddLocation(entry, t.Uri, t.Location);
            AddAuthor(entry, t.CreatedBy == null ? null : t.CreatedBy.Name);

            if (t.DueDate.HasValue)
            {
                entry.Published = t.DueDate.Value;
                AddTime(entry, t.DueDate, t.DueDate, t.AllDay);
            }
            else
                if (t.FinishedAt.HasValue)
                {
                    entry.Published = t.FinishedAt.Value;
                    AddTime(entry, t.FinishedAt, t.FinishedAt, t.AllDay);
                }

            if (t.Flags != null)
                foreach (TasksModel.TaskFlag tf in t.Flags)
                    AddFlag(entry, tf);
            for (int i = entry.Categories.Count - 1; i >= 0; i--)
            {
                AtomCategory c = entry.Categories[i];
                if (c.Term.StartsWith(TasksModel.TaskFlag.BaseSchema))// our category
                {
                    if (t.Flags == null || t.Flags.Count == 0)
                        entry.Categories.RemoveAt(i);
                    else
                        if (null == t.Flags.FirstOrDefault(delegate(TasksModel.TaskFlag f)
                            {
                                return c.Term == t.Uri;
                            }))
                            entry.Categories.RemoveAt(i);
                }
            }

            AddExtProperty(entry, t.UriSchema + "/created", t.CreatedAt.Ticks.ToString());
            AddExtProperty(entry, t.UriSchema + "/changed", t.ChangedAt.Value.Ticks.ToString());
            AddExtProperty(entry, t.UriSchema + "/due",
                t.DueDate.HasValue ? t.DueDate.Value.Ticks.ToString() : "0");
            AddExtProperty(entry, t.UriSchema + "/finished",
                t.FinishedAt.HasValue ? t.FinishedAt.Value.Ticks.ToString() : "0");
            AddExtProperty(entry, t.UriSchema + "/status", string.Format(
                "{0};{1}", t.Status.ID.ToString("D"), t.Status.Name));
            AddExtProperty(entry, t.UriSchema + "/timezone",
                t.TimeZone == null ? string.Empty : t.TimeZone.ToString());
            AddExtProperty(entry, t.UriSchema + "/class", t.Class.ToString());
            AddExtProperty(entry, t.UriSchema + "/order", t.Order.ToString());
            AddExtProperty(entry, t.UriSchema + "/private", t.Private.ToString());
            AddExtProperty(entry, t.UriSchema + "/progress", t.Progress.ToString());
            AddExtProperty(entry, t.UriSchema + "/estimated", t.EstimatedEffort.ToString());
            AddExtProperty(entry, t.UriSchema + "/priority", t.Priority.ToString());
            AddExtProperty(entry, t.UriSchema + "/id", t.ID.ToString("D"));
            AddExtProperty(entry, t.UriSchema + "/timestamp", t.Timestamp.ToString());

            string deps = string.Empty;
            foreach (TasksModel.Task ta in t.Dependencies)
            {
                if (deps.Length > 0)
                    deps += ";";
                deps += ta.ID.ToString("D");
            }
            AddExtProperty(entry, t.UriSchema + "/dependencies", deps);

            //t.Attachments
            //TODO entry.Recurrence
            //t.RecurrenceDetails
            //t.Reocurring
        }

        private static void UpdateTaskFromEvent(TasksModel.Task task, EventEntry entry)
        {
            // overwrite only if we have something
            task.ExternalReference = entry.SelfUri.Content;
            task.Name = entry.Title.Text;
            task.RichContent = entry.Content.Content;
            //task.Location = GetLocation(task.Uri);
            //task.CreatedBy
            //AddAuthor(entry, t.CreatedBy == null ? null : t.CreatedBy.Name);

            task.AllDay = entry.Times[0].AllDay;
            task.EstimatedEffort = ToInt(GetExtProperty(entry, task.UriSchema + "/estimated"));
        }

        private static int ToInt(string value)
        {
            return value == null ? 0 : Convert.ToInt32(value);
        }

        private static string GetExtProperty(EventEntry e, string name)
        {
            ExtendedProperty property =
                e.ExtensionElements.FirstOrDefault(delegate(IExtensionElementFactory el)
                {
                    return (el is ExtendedProperty) && ((ExtendedProperty)el).Name == name;
                }) as ExtendedProperty;
            return property != null ? property.Value : null;
        }

        private static void AddFlag(EventEntry e, TasksModel.TaskFlag tf)
        {
            AtomCategory cat =
                e.Categories.FirstOrDefault(delegate(AtomCategory el)
                {
                    return el.Term == tf.Uri;
                });
            if (cat == null)
            {
                cat = new AtomCategory(tf.Name);
                e.Categories.Add(cat);
            }
            cat.Label = tf.Description;
        }

        private static void AddAuthor(EventEntry e, string name)
        {
            AtomPerson author =
                e.Authors.FirstOrDefault(delegate(AtomPerson el)
                {
                    return el.XmlName == "author";
                });
            if (name != null)
            {
                if (author == null)
                {
                    author = new AtomPerson(AtomPersonType.Author, name);
                    e.Authors.Add(author);
                }
                author.Name = name;
            }
            else
                if (author != null)
                    e.Authors.Remove(author);
        }

        private static void AddLocation(EventEntry e, string name, string location)
        {
            Where loc =
                e.Locations.FirstOrDefault(delegate(Where el)
                {
                    return el.Rel == name;
                });
            if (location != null)
            {
                if (loc == null)
                {
                    loc = new Where();
                    loc.Rel = name;
                    e.Locations.Add(loc);
                }
                loc.ValueString = location;
            }
            else
                if (loc != null)
                    e.Locations.Remove(loc);
        }

        private static void AddTime(EventEntry e, DateTime? start, DateTime? end, bool allDay)
        {
            e.Times.Clear();
            if (start.HasValue)
            {
                When eventTime = new When();
                e.Times.Add(eventTime);
                eventTime.AllDay = allDay;
                eventTime.StartTime = start.Value;
                if (end.HasValue)
                    eventTime.EndTime = end.Value;
                else
                    eventTime.EndTime = start.Value;
            }
        }

        private static void AddExtProperty(EventEntry e, string name, string value)
        {
            ExtendedProperty property =
                e.ExtensionElements.FirstOrDefault(delegate(IExtensionElementFactory el)
                {
                    return (el is ExtendedProperty) && ((ExtendedProperty)el).Name == name;
                }) as ExtendedProperty;
            if (property == null)
            {
                property = new ExtendedProperty();
                property.Name = name;
                e.ExtensionElements.Add(property);
            }
            property.Value = value;
        }

        #endregion
    }
}